Summary
In this article, we will introduce the process of conducting optimization load investigations using Unreal Insights, and how to implement asynchronous loading (background loading) in C++ as a countermeasure to prevent hitches that occur when assets are loaded synchronously with LoadSynchronous()
in the game. For information on asynchronous loading in Blueprints, please refer to the links below.
Environment
- Rider 2024.2.6
- Unreal Engine 5.4
- Windows 11 Pro
References
- Official Documentation Asynchronous Asset Loading
- [UE4] About Asynchronous Loading of Assets in Asset Manager Part 1 (Explanation of Asynchronous Loading & Level Background Loading)
- Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022
- Information on asynchronous loading can be found particularly in the section from 22:40 to 28:15, and the implementation method of asynchronous loading (AsyncLoad) using Blueprints is introduced at 27:33.
- [UE5] Let's Try Using Unreal Insights
- Unreal Insights
Main Content
Optimization Load Investigation Using Unreal Insights
What is Unreal Insights?
Unreal Insights is a tool for analyzing performance and memory usage in Unreal Engine. The official documentation can be found here: https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-in-unreal-engine
How to Use
For detailed usage, please refer to [UE5] Let's Try Using Unreal Insights.
When conducting optimization investigations, it is ideal to use the packaged version of the target platform rather than the regular PIE (Play In Editor) mode. This is because the preloaded cache and background operations in PIE mode can interfere, making accurate load measurement difficult.
This time, we will conduct the investigation in Standalone Game, which is closer to the packaged version environment than PIE.
How to Launch Standalone Game
Launch the Standalone Game from the UE Editor as shown in the figure below.
If hitches (stuttering) occur during gameplay, let's use the following command.
stat unitgraph
The "stat UnitGraph" command is a tool that visualizes the processing load of the game in graph form. This allows you to clearly identify the problem areas, as large spikes will appear on the graph when hitches occur.
Unlike the commonly used "stat Unit" or "stat fps," "stat UnitGraph" records even short-term hitches on the graph, so they are less likely to be missed.
After execution, a graph like the one below will be displayed. The load graph is shown in the lower left, and specific values are displayed in the upper right. Ideally, to achieve 60 fps, it is best to keep the Frame time below 16.6ms.
Next, we will measure using Unreal Insights.
trace.start
and trace.stop
To start measuring with Unreal Insights, execute the trace.start
command. When the measurement is complete, end it with trace.stop
.
Processing Load Measurement Results
As seen in the video below, hitches have been confirmed.
When heavy processing is performed, significant changes appear on the graph.
Game: 58.26ms
We can see that the game thread took 58.26ms.
Next, let's check the measurement results from Unreal Insights.
How to Open Trace
When opening the trace data, the following results are displayed.
The key point in this result is indicated by the green bar:
LoadObject (154.7ms) - /Game/Main/InGame/VFX/Niagara/NS_DizzyStar.NS_DizzyStar
From this, we can see that the loading process of the Niagara effect is placing a significant load.
Especially during the initial loading or spawning of Niagara, a hitch may occur due to shader compilation.
This time, I'll explain the cause of the hitch during the initial loading, but if a hitch occurs during the initial spawning, it can be resolved by spawning in an invisible location beforehand (such as during a blackout or similar scene transition).
In the packaged version, this hitch occurs only the first time after installation.
In UE, it occurs only the first time after UE is started.
If you want to reproduce the hitch, you need to restart Unreal Engine or delete and reinstall the package.
This is the problematic Niagara effect.
Looking at the C++ code, the loading process is as follows.
PlayerCharacter.h1public: 2 //... 3 UPROPERTY(EditAnywhere, BlueprintReadWrite) 4 TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset; 5 //...
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.LoadSynchronous(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
The hitch is caused by synchronously loading the effect asset right before spawning the effect using LoadSynchronous()
.
LoadSynchronous()
(synchronous load) waits for the load to complete (stopping other processes). This leads the player to experience a hitch.
The official UE recommends asynchronous loading.
Reference video: Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022
Asynchronous Loading is from 22:40 to 28:15 in the video.
at 27:33, it shows how to implement asynchronous loading (AsyncLoad) in Blueprint.
TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset
The effect asset is held by the player.
Investigation Result: The cause is LoadSynchronous()
just before spawning the effect.
Implementing Asynchronous Loading (AsyncLoad)
To eliminate the hitch caused by synchronous loading, we will pre-load the asset asynchronously (pre-read) at the start of the game.
Asynchronous loading may take time, so please ensure there is enough time for loading, otherwise you cannot spawn the asset.
Next, we will create a function for asynchronous loading in C++.
PlayerCharacter.h1protected: 2 void OnDizzyEffectLoaded(); 3 void LoadDizzyEffectAsset();
PlayerCharacter.cpp1void APlayerCharacter::BeginPlay() 2{ 3 Super::BeginPlay(); 4 LoadDizzyEffectAsset(); 5} 6 7void APlayerCharacter::LoadDizzyEffectAsset() 8{ 9 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset requeset load")); 10 UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(DizzyEffectAsset.ToSoftObjectPath(), 11 FStreamableDelegate::CreateUObject( 12 this, &APlayerCharacter::OnDizzyEffectLoaded)); 13} 14 15void APlayerCharacter::OnDizzyEffectLoaded() 16{ 17 UE_LOG(LogTemp, Log, TEXT("DizzyEffectLoaded")); 18 19 if (IsValid(DizzyEffectAsset.Get())) 20 { 21 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset is valid")); 22 } 23 else 24 { 25 UE_LOG(LogTemp, Error, TEXT("DizzyEffectAsset is null, Function name: %s"), *FString(__FUNCTION__)); 26 } 27}
Next, replace the synchronous asset loading with Get()
.
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.Get(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
This completes the implementation of asynchronous loading.
Next, let's verify it in the Standalone game.
If the log shows DizzyEffectAsset is valid
, it confirms that the asynchronous loading (AsyncLoad) of the asset was successful.
Results
As a result of using the "stat UnitGraph" command, the spikes on the graph have disappeared, and the hitch has been resolved.
Summary
Investigation Process
- Hitch Detection: We used the "stat unitgraph" command to graph the hitches that occurred during gameplay and identify where heavy processing was taking place.
- Measurement with Unreal Insight: To investigate the detailed load during hitch occurrences, we collected trace data using the trace.start and trace.stop commands and confirmed the source of the load.
Problem Identification
- It was determined that the effect asset (Niagara effect) was being loaded using synchronous loading (
LoadSynchronous()
), causing other processes to stop until the load was complete, resulting in hitches.
Implementation of Asynchronous Loading
- To avoid hitches, we added a process to asynchronously load the effect at the start of the game using TSoftObjectPtr. This ensures that the asset is loaded before the player uses the effect, preventing hitches.
Result
The implementation of asynchronous loading confirmed that the hitches were resolved, resulting in smoother gameplay. By using asynchronous loading, we improved game performance and enhanced the player experience.